<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>观察者模式</title>
</head>
<body>
<script>
    /**
     * 观察者模式，又叫发布-订阅模式
     * 定义对象间一对多的关系，但一个对象信息发生变化时，所有依赖于它的对象都将得到通知
     * JS中的事件模型使用的就是发布-订阅模式，按钮绑定点击事件后，当事件发生时，就会通知订阅者
     *
     * 除了DOM事件，我们还经常会实现一些自定义事件
     * 1.首先要确定谁充当发布者
     * 2.给发布者添加一个缓存列表，用于存放回调以便通知订阅者
     * 3.发布消息时，发布者遍历缓存列表，依次触发回调
     * */

        // 最简单的发布-订阅模式实现
    const salesOffices = {}

    // 缓存列表，存放订阅者的回调
    salesOffices.clientList = []

    // 增加订阅者，订阅的消息添加进缓存列表
    salesOffices.listen = function (fn) {
        this.clientList.push(fn)
    }

    // 消息发布
    salesOffices.trigger = function () {
        for (let i = 0, fn; fn = this.clientList[i++];) {
            fn.apply(this, arguments)
        }
    }

    // 最简单的发布-订阅模式存在的问题是没有明确消息类型，可增加一个key
    // 让订阅者只订阅自己感兴趣的消息
    salesOffices.listen = function (key, fn) {
        if (!this.clientList[key]) {
            this.clientList[key] = []
        }

        this.clientList[key].push(fn)
    }

    salesOffices.trigger = function () {
        const key = Array.prototype.shift.call(arguments)
        const fns = this.clientList[key]

        if (!fns || fns.length === 0) {
            return false
        }

        for (let i = 0, fn; fn = fns[i++];) {
            fn.apply(this, arguments)
        }
    }


    /**
     * 发布-订阅模式的通用实现
     * 将发布-订阅的功能提取出来，放在一个单独对象中
     * */
    const event = {
        clientList: [],
        listen: function (key, fn) {
            if (!this.clientList[key]) {
                this.clientList[key] = []
            }

            this.clientList[key].push(fn)
        },
        trigger: function () {
            const key = Array.prototype.shift.call(arguments)
            const fns = this.clientList[key]

            if (!fns || fns.length === 0) {
                return false
            }

            for (let i = 0, fn; fn = this.clientList[i++];) {
                fn.apply(this, arguments)
            }
        },
        // 取消订阅的事件
        remove: function (key, fn) {
            const fns = this.clientList[key]

            if (!fns || fns.length === 0) return false

            // 如果没有传入具体回调，则表示取消key对应的所有订阅
            if (!fn) {
                fns && (fns.length = 0)
            } else {
                const i = this.clientList.indexOf(fn)

                if (i <= -1) return false

                this.clientList.splice(i, 1)
            }
        }
    }

    // 定义installEvent函数，用于给所有对象都动态安装发布-订阅功能
    function installEvent(obj) {
        for (let k in event) {
            obj[k] = event[k]
        }
    }


    /**
     * 真实案例
     * 网站导航栏、消息列表、购物车等模块都需要用到用户信息
     * 如果是请求到用户信息后在回调里将信息更新到这些模块，这种做法就产生了强耦合
     * 这种耦合性让程序变得僵硬，用到的模块的方法不可随意更改，否则可能出现改漏的情况
     * 如果要添加新业务如收货地址管理模块，则需要深入到请求回调里进行修改
     *
     * 如果用发布-订阅模式改写后，登录模块只需发布登录成功的消息，而业务方收到消息后
     * 就开始各自的业务处理，登录模块不再需要关心业务方内部细节
     * 如要加上收货地址管理模块，只需要在该模块内订阅消息即可
     *
     * 上面发布-订阅模式的通用实现，给每个发布对象都添加了listen和trigger方法，还有
     * 一个缓存列表，这是一种资源浪费，所以，发布-订阅模式可用一个全局Event对象来实现
     * 订阅者不需要了解消息来自哪个发布者，而发布者也不需要知道消息会推送给哪些订阅者
     * */
    const Event = (function () {
        const clientList = {}

        function listen(key, fn) {
            if (!clientList[key]) {
                clientList[key] = []
            }

            clientList[key].push(fn)
        }

        function trigger() {
            const key = Array.prototype.shift.call(arguments)
            const fns = clientList[key]

            if (!fns || fns.length === 0) return false

            for (let i = 0, fn; fn = clientList[i++];) {
                fn.apply(this, arguments)
            }
        }

        function remove(key, fn) {
            const fns = clientList[key]

            if (!fns || fns.length === 0) return false

            // 具体的回调不传则清空该key所有回调
            if (!fn) {
                fns && (fns.length = 0)
            } else {
                const i = fns.findIndex(fn)

                if (i <= -1) return false

                fns.splice(i, 1)
            }
        }

        return {
            listen,
            trigger,
            remove
        }
    }())


    /**
     * 全局的发布-订阅模式可在两个封装良好的模块间进行通信
     * 但要注意如果模块间使用过多的全局发布-订阅模式，我们最终会搞不清
     * 消息来自哪个模块，消息会流向哪些模块，这会给维护带来麻烦
     *
     * 目前的发布-订阅模式，订阅者都必须先订阅一个消息，随后才能接收到发布者发布的消息
     * 而有时候需要有先发布后订阅的能力，如QQ的离线消息，离线消息被保存在服务器
     * 接收人下次登录时，可重新收到这条消息
     * 要满足这个需求，要建立一个存放离线事件的栈，事件发布时，若还没订阅者订阅这事件
     * 则暂时把发布事件的动作包裹在一个函数中，存入栈中，等有订阅者订阅此事件时
     * 再遍历栈并依次执行这些包装函数，也就是重新发布里面的事件
     * 离线事件的生命周期只有一次，所以刚才的操作只能进行一次
     *
     * 还需要给Event对象提供创建命名空间的功能
     * 否则消息名过多时可能会出现冲突的情况
     * */
    const Event = (function () {
        let global = this
        let Event
        let _default = 'default'

        Event = function () {
            let _listen
            let _trigger
            let _remove
            let _slice = Array.prototype.slice
            let _shift = Array.prototype.shift
            let _unshift = Array.prototype.unshift
            let namespaceCache = {}
            let _create
            let find
            let each = function (arg, fn) {
                let ret
                for (let i = 0; i < arg.length; i++) {
                    let n = arg[i]
                    ret = fn.call(n, i, n)
                }
                return ret
            }

            _listen = function (key, fn, cache) {
                if (!cache[key]) {
                    cache[key] = []
                }
                cache[key].push(fn)
            }

            _remove = function (key, cache, fn) {
                if (cache[key]) {
                    if (fn) {
                        for (let i = cache[key].length; i >= 0; i--) {
                            if (cache[key] === fn) {
                                cache[key].splice(i, 1)
                            }
                        }
                    }
                } else {
                    cache[key] = []
                }
            }

            _trigger = function () {
                let cache = _shift.call(arguments)
                let key = _shift.call(arguments)
                let args = arguments
                let _self = this
                let ret
                let stack = cache[key]

                if (!stack || !stack.length) return

                return each(stack, function () {
                    return this.apply(_self, args)
                })
            }

            _create = function (namespace = _default) {
                let cache = {}
                let offlineStack = [] // 离线事件
                let ret = {
                    listen(key, fn, last) {
                        _listen(key, fn, cache)

                        if (offlineStack === null) return

                        if (last = 'last') {
                            offlineStack.length && offlineStack.pop()()
                        } else {
                            each(offlineStack, function () {
                                this()
                            })
                        }
                        offlineStack = null
                    },
                    one(key, fn, last) {
                        _remove(key, cache)
                        this.listen(key, fn, last)
                    },
                    remove(key, fn) {
                        _remove(key, cache, fn)
                    },
                    trigger() {
                        let fn
                        let args
                        let _self = this

                        _unshift.call(arguments, cache)
                        args = arguments

                        fn = function () {
                            return _trigger.apply(_self, args)
                        }

                        if (offlineStack) {
                            return offlineStack.push(fn)
                        }
                        return fn()
                    }
                }

                return namespace ?
                    (namespaceCache[namespace] ?
                        namespaceCache[namespace] :
                        namespaceCache[namespace] = ret) : ret
            }

            return {
                create: _create,
                one(key, fn, last) {
                    let event = this.create()
                    event.one(key, fn, last)
                },
                remove(key, fn) {
                    let event = this.create()
                    event.remove(key, fn)
                },
                listen(key, fn, last) {
                    let event = this.create()
                    event.listen(key, fn, last)
                },
                trigger() {
                    let event = this.create()
                    event.trigger.apply(this.arguments)
                }
            }
        }()

        return Event
    }())
</script>
</body>
</html>